/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import Unsafe from './Unsafe';
import Long from '../util/long/index'

export default class XxHash64 {
    private static PRIME64_1: Long = Long.fromString('-7046029288634856825');
    private static PRIME64_2: Long = Long.fromString('-4417276706812531889');
    private static PRIME64_3: Long = Long.fromString('1609587929392839161');
    private static PRIME64_4: Long = Long.fromString('-8796714831421723037');
    private static PRIME64_5: Long = Long.fromString('2870177450012600261');

    constructor() {
    }

    public static hash(seed: Long, base: any, address: Long, length: number): Long {
        let hash: Long;
        if (length >= 32) {
            hash = this.updateBody(seed, base, address, length);
        }
        else {
            hash = seed.add(this.PRIME64_5);
        }

        hash = hash.add(length);

        let index: number = length & -32;

        return this.updateTail(hash, base, address, index, length);
    }

    private static updateTail(hash: Long, base: any, address: Long, index: number, length: number): Long {
        while (index <= length - 8) {
            hash = this.updateTailP14(hash, Unsafe.getLong(base, address.add(index).toNumber()));
            index += 8;
        }

        if (index <= length - 4) {
            hash = this.updateTailP2(hash, Unsafe.getInt(base, address.add(index).toNumber()));
            index += 4;
        }

        while (index < length) {
            hash = this.updateTailP1(hash, Unsafe.getByte(base, address.add(index).toNumber()));
            index++;
        }

        hash = this.finalShuffle(hash);

        return hash;
    }

    private static updateBody(seed: Long, base: any, address: Long, length: number): Long
    {
        let v1: Long = seed.add(XxHash64.PRIME64_1).add(XxHash64.PRIME64_2);
        let v2: Long = seed.add(XxHash64.PRIME64_2);
        let v3: Long = seed;
        let v4: Long = seed.subtract(XxHash64.PRIME64_1);

        let remaining: number = length;
        while (remaining >= 32) {
            v1 = this.mix(v1, Unsafe.getLong(base, address.toNumber()));
            v2 = this.mix(v2, Unsafe.getLong(base, address.add(8).toNumber()));
            v3 = this.mix(v3, Unsafe.getLong(base, address.add(16).toNumber()));
            v4 = this.mix(v4, Unsafe.getLong(base, address.add(24).toNumber()));

            address = address.add(32);
            remaining -= 32;
        }

        let hash: Long =
        XxHash64.rotateLeft(v1, 1)
            .add(XxHash64.rotateLeft(v2, 7))
            .add(XxHash64.rotateLeft(v3, 12))
            .add(XxHash64.rotateLeft(v4, 18));

        hash = XxHash64.update(hash, v1);
        hash = XxHash64.update(hash, v2);
        hash = XxHash64.update(hash, v3);
        hash = XxHash64.update(hash, v4);

        return hash;
    }

    private static mix(current: Long, value: Long): Long{
        let a = value.multiply(XxHash64.PRIME64_2)
        let b = current.add(a)
        return XxHash64.rotateLeft(b, 31).multiply(XxHash64.PRIME64_1);
    }

    public static rotateLeft(i: Long, distance: number): Long {
        return i.shiftLeft(distance).or(i.shiftRightUnsigned(-distance));
    }

    private static update(hash: Long, value: Long): Long {
        let temp: Long = hash.xor(XxHash64.mix(Long.fromNumber(0), value));
        return temp.multiply(XxHash64.PRIME64_1).add(XxHash64.PRIME64_4);
    }

    private static updateTailP14(hash: Long, value: Long): Long{
        let temp: Long = hash.xor(XxHash64.mix(Long.fromNumber(0), value));
        return this.PRIME64_1.multiply(XxHash64.rotateLeft(temp, 27)).add(this.PRIME64_4);
    }

    private static updateTailP2(hash: Long, value: number): Long{
        let unsigned: Long = Long.fromNumber(value).and(4294967295);
        let temp: Long = hash.xor((unsigned.multiply(XxHash64.PRIME64_1)));
        return this.PRIME64_2.multiply(XxHash64.rotateLeft(temp, 23)).add(XxHash64.PRIME64_3);
    }

    private static updateTailP1(hash: Long, value: number): Long
    {
        let unsigned: number = value & 0xFF;
        let temp: Long = hash.xor((Long.fromNumber(unsigned).multiply(XxHash64.PRIME64_5)));
        return this.PRIME64_1.multiply(XxHash64.rotateLeft(temp, 11));
    }

    private static finalShuffle(hash: Long): Long {
        hash = hash.xor(hash.shiftRightUnsigned(33));
        hash = hash.multiply(XxHash64.PRIME64_2);
        hash = hash.xor(hash.shiftRightUnsigned(29));
        hash = hash.multiply(XxHash64.PRIME64_3);
        hash = hash.xor(hash.shiftRightUnsigned(32));
        return hash;
    }
}
